using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Drawing;
using System.Configuration;
using System.Collections;
using System.Data;


using BizElements.BusinessLayer;

namespace BizElements.Web
{
	/// <summary>
	/// ListBox control bound to a business class property.
	/// </summary>
	[DefaultProperty("Text"), ToolboxData("<{0}:BoundListBox runat=server></{0}:BoundListBox>")]
    public class BoundListBox : System.Web.UI.WebControls.ListBox, IBoundControl, IValidator, IBoundListControl
	{
        Label errorLbl = new Label();

		#region FieldPermissions.

		FieldAccessMode accessMode = FieldAccessMode.Automatic;
		string editCss = "";
		string readOnlyCss = "";

		IBindableField bizField;
        string additionalTextFieldsSemicolDelimited = "";
        string displayFormat = "{0}";

        Color errorColor = Color.Red;
		ErrorLocation errorLocation = ErrorLocation.Below;
		
		#endregion

		#region Constructors.

		/// <summary>
		/// Initializes a new instance of the <see cref="BoundListBox"/> class.
		/// </summary>
		public BoundListBox()
		{
		}

		#endregion

        #region OnInit.

        /// <summary>
        /// Raises the <b>Init</b> event.
        /// </summary>
        /// <param name="e">An <b>EventArgs</b> object that contains the event data.</param>
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            AddMyselfToParentPageValidators();
        }

        private void AddMyselfToParentPageValidators()
		{			
			this.Page.Validators.Add(this);
        }

        #endregion

        #region Control properties.

        /// <summary>
		/// Gets or sets the Cascading Style Sheet (CSS) class rendered by the Web server control on the client.
		/// </summary>
		[Bindable(false), Category("Behavior"), DefaultValue(typeof(string), ""),
		Description("Cascading Style Sheet (CSS) class rendered by the Web server control on the client.")]
		public override string CssClass
		{
			get
			{
				return this.editCss;
			}
			set
			{
				this.editCss = (value != null) ? value.Trim() : "";
				base.CssClass = this.editCss;
			}
		}

		/// <summary>
		/// Gets or sets the Cascading Style Sheet (CSS) class rendered by the Web server control on the client 
        /// when the control is in read-only mode.
		/// </summary>
		[Bindable(false), Category("Behavior"), DefaultValue(typeof(string), ""),
		Description("Cascading Style Sheet (CSS) class rendered by the Web server control on the client when the control is in read-only mode.")]
		public string ReadOnlyCssClass
		{
			get {return this.readOnlyCss;}
			set {this.readOnlyCss = (value != null) ? value.Trim() : "";}
		}

        /// <summary>
        /// Gets or sets additional semicolon delimited fields of the data source that provide the text content 
        /// of the list items. The value which is displayed int the control is a combination of 
        /// <see cref="ListControl.DataTextField"/> and the provided list formattedas defined by <see cref="DisplayFormat"/>.
        /// </summary>
        [Bindable(false), Category("Behavior"), DefaultValue(typeof(string), ""),
        Description("Additional semicolon delimited fields of the data source that provide the text content of the list items.")]
        public string AdditionalTextFields
        {
            get { return this.additionalTextFieldsSemicolDelimited; }
            set { this.additionalTextFieldsSemicolDelimited = (value != null) ? value.Trim() : ""; }
        }

        /// <summary>
        /// Gets or sets the format which specifies how <see cref="ListControl.DataTextField"/> and 
        /// <see cref="AdditionalTextFields"/> are concatenated and formatted.
        /// </summary>
        /// <remarks>The specified value must obey the same format as the format used by
        /// <see cref="string.Format(string, object[])"/> method.</remarks>
        [Bindable(false), Category("Behavior"), DefaultValue(typeof(string), "{0}"),
        Description("Specifies how DataTextField and AdditionalTextFields are concatenated and formatted.")]
        public string DisplayFormat
        {
            get { return this.displayFormat; }
            set { this.displayFormat = string.IsNullOrEmpty(value) ? "{0}" : value.Trim(); }
        }

        bool fieldCaptionIsDefaultToolTip = true;

        /// <summary>
        /// Gets or sets whether the caption of the bound field is used as default tooltip.
        /// </summary>
        /// <value><b>true</b> if field caption is default tooltop; <b>false</b> otherwise. Default is <b>true</b>.</value>
        [Bindable(false)]
        [Category("Behavior")]
        [DefaultValue(true)]
        [Description("Whether the caption of the bound field is used as default tooltip.")]
        public virtual bool FieldCaptionIsDefaultToolTip
        {
            get { return this.fieldCaptionIsDefaultToolTip; }
            set { this.fieldCaptionIsDefaultToolTip = value; }
        }

		#endregion

        #region BindDataSource.

        /// <summary>
        /// Sets and binds a datasource to the control.
        /// </summary>
        /// <param name="dataSource">The object from which the control retrieves its list of data items.</param>
        /// <param name="dataValueField">The field of the data source that provides the value of each list item.</param>
        /// <param name="dataTextField">The field of the data source that provides the text content of the list items.</param>
        public void BindDataSource(object dataSource, string dataValueField, string dataTextField)
        {
            this.DataSource = dataSource;
            this.DataValueField = dataValueField;
            this.DataTextField = dataTextField;
            DataBind();
        }

        /// <summary>
        /// Sets and binds a datasource to the control.
        /// </summary>
        /// <param name="dataSource">The object from which the control retrieves its list of data items.</param>
        public void BindDataSource(ICodeList dataSource)
        {
            BindDataSource(dataSource, dataSource.ValueMember, dataSource.DisplayMember);
        }

        #endregion

        #region DataBind with additional fields.

        private string[] GetAdditionalTextFields()
        {
            string[] rawFields = this.additionalTextFieldsSemicolDelimited.Split(';');
            ArrayList fixedFields = new ArrayList();
            foreach (string field in rawFields)
            {
                if (!string.IsNullOrEmpty(field))
                    fixedFields.Add(field);
            }

            string[] additionalField = new string[fixedFields.Count];
            for (int fieldIdx = 0; fieldIdx < fixedFields.Count; fieldIdx++)
                additionalField[fieldIdx] = (string)fixedFields[fieldIdx];

            return additionalField;
        }

        /// <summary>
        /// Binds a data source to the invoked server control and all its child controls.
        /// </summary>
        public override void DataBind()
        {
            string[] additionalTextFields = GetAdditionalTextFields();
            IList data = null;
            if (this.DataSource is IList)
                data = this.DataSource as IList;
            else if (this.DataSource is IListSource)
                data = (this.DataSource as IListSource).GetList();

            if (additionalTextFields.Length > 0 && data != null)
            {
                DataTable formattedDataSource = new DataTable();
                formattedDataSource.Columns.Add(this.DataValueField);
                formattedDataSource.Columns.Add(this.DataTextField);

                foreach (object obj in data)
                {
                    object keyValue = DataBinder.Eval(obj, this.DataValueField);

                    string[] textValues = new string[1 + additionalTextFields.Length];
                    object firstVal = DataBinder.Eval(obj, this.DataTextField);
                    textValues[0] = (firstVal != null) ? firstVal.ToString() : "";
                    for (int fieldIdx = 0; fieldIdx < additionalTextFields.Length; fieldIdx++)
                    {
                        object currval = DataBinder.Eval(obj, additionalTextFields[fieldIdx]);
                        textValues[fieldIdx + 1] = (currval != null) ? currval.ToString() : "";
                    }

                    string formattedTextValue = string.Format(this.displayFormat, textValues);
                    formattedDataSource.Rows.Add(keyValue, formattedTextValue);
                }

                this.DataSource = formattedDataSource;
            }

            base.DataBind();
        }

        #endregion

		#region Error properties.

        /// <summary>
        /// Gets or sets the CSS Class name applied to the error label.
        /// </summary>
        [Bindable(false), Category("Appearance"), DefaultValue(null),
        Description("CSS Class name applied to the error label.")]
        public virtual string ErrorCss
        {
            get { return this.errorLbl.CssClass; }
            set { this.errorLbl.CssClass = value; }
        }

        /// <summary>
        /// Gets or sets the color of the error text.
        /// </summary>
        [Bindable(false), Category("Appearance"), DefaultValue(typeof(Color), "Red"),
        Description("Color of the error text.")]
        [TypeConverter(typeof(WebColorConverter))]
        public virtual Color ErrorColor
        {
            get { return this.errorColor; }
            set { this.errorColor = value; }
        }

        /// <summary>
        /// Gets or sets the error text displayed in the error label or in the page's validation summary. 
        /// Usually set programmatically.
        /// </summary>
        [Bindable(false), Category("Appearance"),
        Description("Error text displayed in the error label or in the page's validation summary. Usually set programmatically.")]
        [DefaultValue("")]
        public virtual string ErrorText
        {
            get { return this.errorLbl.Text; }
            set { this.errorLbl.Text = value; }
        }

        /// <summary>
        /// Gets or sets the indicator placed by the control if it contains an invalid value. 
        /// Displayed only when the error text is displayed in the validation summary.
        /// </summary>
        [Bindable(false), Category("Appearance"), DefaultValue("*"),
        Description("Indicator placed by the control if it contains an invalid value. Displayed only when the error text is displayed in the validation summary.")]
        public virtual string ErrorIndicator
        {
            get { return "*"; }
            set { }
        }

        /// <summary>
        /// Gets or sets the value which specifies where the error text is located.
        /// </summary>
        [Bindable(false), Category("Appearance"), DefaultValue(typeof(ErrorLocation), "Below"),
        Description("Specifies where the error text is located.")]
        public virtual ErrorLocation ErrorLocation
        {
            get { return this.errorLocation; }
            set { this.errorLocation = value; }
        }

		#endregion

		#region IBoundControl.

		/// <summary>
		/// Gets or sets the value which indicates whether the <b>BoundListBox</b> can change data in the bound field.
		/// </summary>
		/// <remarks>Setting this property to <b>ReadOnly</b> will prevent users from changing the selected item.</remarks>
		[Bindable(false), Category("Behavior"), DefaultValue(typeof(FieldAccessMode), "Automatic"),
		Description("Indicates whether the contents of the control can be changed.")]
		public FieldAccessMode AccessMode
		{
			get {return this.accessMode;}
			set {this.accessMode = value;}
		}

		/// <summary>
		/// Gets or sets the business class' property to which the control is bound. Set programmatically.
		/// </summary>
		[Description("Business class' property to which the control is bound. Set programmatically.")]
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public virtual IBindableField BusinessField
		{
			get {return this.bizField;}
			set {this.bizField = value;}
		}

		/// <summary>
		/// Appends all errors in the collection to the <see cref="ErrorText"/>.
		/// </summary>
		/// <param name="rules">Collection of broken rules.</param>
		/// <param name="formatter">The <see cref="IMessageFormatter"/> object used to format the messages contained in the collection.</param>
		public virtual void AppendError(BusinessRuleCollection rules, IMessageFormatter formatter)
		{
			BusinessRuleCollection brokenRulesForThisControl = rules.GetRulesForField(this.BusinessField.BindingName, true);
			this.ErrorText += brokenRulesForThisControl.ToString(formatter) + " ";
		}

		/// <summary>
		/// Clears errors. Same as setting the <see cref="ErrorText"/> to empty string.
		/// </summary>
		public virtual void ClearError()
		{
			this.ErrorText = "";
		}

		#endregion

		#region IValidator.

		/// <summary>
		/// Gets the value contained in the <see cref="ErrorText"/> property if a validation summary has been specified as location for errors.
		/// </summary>
		string IValidator.ErrorMessage
		{
			get 
			{
				if (this.errorLocation == ErrorLocation.ValidationSummary)
					return this.ErrorText;
				else
					return "";
			}
			set 
			{				
			}
		}		

		/// <summary>
		/// Does nothing. Validation logic is performed elsewhere (eg. business class).
		/// </summary>
		void IValidator.Validate()
		{
		}

		/// <summary>
		/// Gets a value which indicates whether an the <see cref="ErrorText"/> property contains error description.
		/// </summary>
		bool IValidator.IsValid
		{
			get
			{
				if (this.ErrorText == null  ||  this.ErrorText.Trim() == "")
					return true;
				else
					return false;
			}
			set 
			{
			}
		}

		#endregion

        #region TrySelectValue.

        /// <summary>
        /// Tries to select the item that contains the specified value.
        /// </summary>
        /// <param name="value">The value of the item to select.</param>
        /// <returns><b>true</b> if item that contains the specified value exists and is selected; <b>false</b> otherwise.</returns>
        public virtual bool TrySelectValue(string value)
        {
            ListItem itemToSelect = this.Items.FindByValue(value);
            bool exists = (itemToSelect != null);
            if (exists)
                this.SelectedValue = itemToSelect.Value;

            return exists;
        }

        #endregion

		#region Render.

		/// <summary>
		/// Sends server control content to a provided <see cref="HtmlTextWriter"/> object, which writes the content to be rendered on the client.
		/// </summary>
		/// <param name="output">The <b>HtmlTextWriter</b> object that receives the server control content.</param>
		protected override void Render(HtmlTextWriter output)
		{
            if (IsInReadOnlyMode())
            {
                RenderAsReadOnlyLabel(output);
            }
            else
            {
                if (this.FieldCaptionIsDefaultToolTip && string.IsNullOrEmpty(this.ToolTip) && this.bizField != null)
                    this.ToolTip = bizField.Caption;

                base.Render(output);
            }

            RenderErrorMessage(output);		
		}

		private bool IsInReadOnlyMode()
		{
			bool isReadOnly;
			switch (this.AccessMode)
			{
				case FieldAccessMode.Automatic:
					if (this.bizField != null  &&  this.bizField.IsReadOnly)
						isReadOnly = true;
					else
						isReadOnly = false;
					break;
				case FieldAccessMode.Edit:
					isReadOnly = false;
					break;
				case FieldAccessMode.ReadOnly:
					isReadOnly = true;
					break;
				default:
					throw new Exception("Unsupported field access mode: " + this.accessMode.ToString() + ".");
			}

			return isReadOnly;
		}

		private void RenderAsReadOnlyLabel(HtmlTextWriter output)
		{
			Label lbl = new Label();
			lbl.CopyBaseAttributes(this);
			lbl.CssClass = (this.readOnlyCss != "") ? this.readOnlyCss : this.editCss;
			lbl.Text = this.SelectedItem.Text;
			lbl.BackColor = this.BackColor;
			lbl.RenderControl(output);
		}

        private void RenderErrorMessage(HtmlTextWriter output)
        {
            if (this.errorLbl.Text != null && this.errorLbl.Text.Trim() != "")
            {
                this.errorLbl.ForeColor = this.ErrorColor;
                this.errorLbl.EnableViewState = false;
                switch (this.errorLocation)
                {
                    case ErrorLocation.Below:
                        output.Write("<br />");
                        break;

                    case ErrorLocation.Right:
                        output.Write("&nbsp;");
                        break;

                    case ErrorLocation.ValidationSummary:
                        output.Write("&nbsp;");
                        break;
                }

                this.errorLbl.RenderControl(output);
            }
        }

		#endregion

        /// <summary>Gets the name of the control property which is bound two-way to a business object field: <b>SelectedValue</b></summary>
        public string TwoWayBoundProperty
        {
            get { return "SelectedValue"; }
        }

        /// <summary>Returns <b>null</b>.</summary>
        public string[] OneWayBoundProperties
        {
            get { return null; }
        }

        /// <summary>Gets error label control which displays errors associated with the given field.</summary>
        [Browsable(false)]
        public ITextControl ErrorLabel
        {
            get { return this.errorLbl; }
        }

        /// <summary>Not implemented. Returns <b>null</b>.</summary>
        [Browsable(false)]
        ITextControl IBoundControl.CaptionLabel
        {
            get { return null; }
        }

        /// <summary>Not implemented. Returns <b>null</b>.</summary>
        [Browsable(false)]
        ITextControl IBoundControl.TooltipLabel
        {
            get { return null; }
        }

        /// <summary>Not implemented. Returns <b>null</b>.</summary>
        [Browsable(false)]
        Control IBoundControl.RequiredFieldIndicatorControl
        {
            get { return null; }
        }
	}
}